﻿using System.IO;
using System.Reflection;
using System.Linq;
using System;
using System.Collections.Generic;
using EmbeddedSensorCloud.Plugin;
using System.Text;

namespace EmbeddedSensorCloud.Core.Plugin
{
    public class PluginCore
    {
        private readonly HashSet<AvailablePlugin> plugins;

        /// <summary>
        /// A readonly plugin enumeration.
        /// </summary>
        public IEnumerable<AvailablePlugin> Plugins
        {
            get { return this.plugins; }
        }

        /// <summary>
        /// Initialize a PluginHandler with an empty plugin collection.
        /// </summary>
        public PluginCore()
        {
            this.plugins = new HashSet<AvailablePlugin>();
        }

        /// <summary>
        /// Tries to add a plugin and returns true when succeeded.
        /// </summary>
        /// <param name="path">The path of the plugin.</param>
        /// <returns>Returns if add operation succeeded.</returns>
        public AvailablePlugin AddPlugin(string path, string[] args = null)
        {
            if (path == null)
            {
                throw new ArgumentException("Plugin path cannot be null");
            }

            if (!File.Exists(path))
            {
                throw new FileNotFoundException("Plugin path must be an existing file");
            }

            Assembly assembly = null;

            try
            {
                var rawBytes = File.ReadAllBytes(path);
                assembly = Assembly.Load(rawBytes);
            }
            catch (ArgumentException)
            {
                throw new PluginLoadException("Plugin path cannot be null.");
            }
            catch (FileNotFoundException) 
            {
                throw new PluginLoadException(string.Format("Invalid file path: '{0}'", path));
            }
            catch (Exception e)
            {
                throw new PluginLoadException(e.Message);
            }

            foreach (var assemblyType in assembly.GetExportedTypes().Where(A => !A.IsAbstract))
            {
                var typeInterface = assemblyType.GetInterface("IPlugin", false);
                if (typeInterface != null)
                {
                    Type pluginType = assembly.GetType(assemblyType.ToString());

                    #region AlreadyExistingCheck
                    foreach (var addedPlugin in this.plugins)
                    {
                        if (addedPlugin.Type.FullName.Equals(pluginType.FullName))
                        {
                            throw new PluginExistingException(addedPlugin.Name, addedPlugin.Version);
                        }
                    }
                    #endregion

                    string pluginName = this.GetAssemblyTitle(assembly);
                    Version pluginVersion = assembly.GetName().Version;
                    IPlugin instance = null;

                    #region Activation
                    try
                    {
                        if (args == null || !args.Any()) 
                        {
                            instance = (IPlugin)Activator.CreateInstance(pluginType);
                        }
                        else 
                        {
                            instance = (IPlugin)Activator.CreateInstance(pluginType, args);
                        }
                    }
                    catch (Exception e)
                    {
                        throw new PluginActivationException("Plugin creation failed", e, path, pluginName, pluginVersion, pluginType);
                    }
                    #endregion

                    #region Initialization
                    try
                    {
                        instance.Initialize();
                    }
                    catch (Exception e)
                    {
                        throw new PluginInitializationException("Plugin initialization failed", e, path, pluginName, pluginVersion);
                    }
                    #endregion

                    var plugin = new AvailablePlugin(pluginType, path, instance, pluginName, pluginVersion);
                    this.plugins.Add(plugin);
                    return plugin;
                }
            }

            return null;
        }

        /// <summary>
        /// Tries to remove a plugin and return true whether succeeded or not.
        /// </summary>
        /// <param name="plugin">The plugin to remove.</param>
        /// <returns>Returns if remove operation succeeded.</returns>
        public bool RemovePlugin(AvailablePlugin plugin)
        {
            if (plugin == null)
            {
                throw new ArgumentException("Plugin cannot be null");
            }

            return this.plugins.Remove(plugin);
        }

        public bool RemovePlugin(string pluginName)
        {
            if (string.IsNullOrWhiteSpace(pluginName))
            {
                throw new ArgumentException("Plugin name cannot be null, empty or whitespaced.");
            }

            foreach (var plugin in this.plugins)
            {
                if (plugin.Name == pluginName)
                {
                    return this.RemovePlugin(plugin);
                }
            }

            return false;
        }

        /// <summary>
        /// Clears all available plugins.
        /// </summary>
        /// <returns>Returns the number of removed plugins.</returns>
        public int ClearPlugins()
        {
            int removedPlugins = this.plugins.Count;
            this.plugins.Clear();
            return removedPlugins;
        }

        /// <summary>
        /// Gets the assembly title.
        /// </summary>
        /// <returns>The assembly title.</returns>
        private string GetAssemblyTitle(Assembly assembly)
        {
            // Get all Title attributes on this assembly
            object[] attributes = assembly.GetCustomAttributes(typeof(AssemblyTitleAttribute), false);
            // If there is at least one Title attribute
            if (attributes.Length > 0)
            {
                // Select the first one
                AssemblyTitleAttribute titleAttribute = (AssemblyTitleAttribute)attributes[0];
                // If it is not an empty string, return it
                if (!string.IsNullOrWhiteSpace(titleAttribute.Title))
                {
                    return titleAttribute.Title;
                }
            }
            // If there was no Title attribute, or if the Title attribute was the empty string, return the .exe name
            return System.IO.Path.GetFileNameWithoutExtension(Assembly.GetExecutingAssembly().CodeBase);
        }
    }
}
